/*
 * Copyright (c) 1999, 2006, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.misc;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import zce.app.compile.helper.ParseUtil;
import android.annotation.SuppressLint;

/**
 * <p>
 * This class checks dependent extensions a particular jar file may have
 * declared through its manifest attributes.
 * </p>
 * Jar file declared dependent extensions through the extension-list attribute.
 * The extension-list contains a list of keys used to fetch the other attributes
 * describing the required extension. If key is the extension key declared in
 * the extension-list attribute, the following describing attribute can be found
 * in the manifest : key-Extension-Name: (Specification package name)
 * key-Specification-Version: (Specification-Version)
 * key-Implementation-Version: (Implementation-Version)
 * key-Implementation-Vendor-Id: (Imlementation-Vendor-Id)
 * key-Implementation-Version: (Implementation version) key-Implementation-URL:
 * (URL to download the requested extension)
 * <p>
 * This class also maintain versioning consistency of installed extensions
 * dependencies declared in jar file manifest.
 * </p>
 * 
 * @author Jerome Dochez
 */
public class ExtensionDependency {

	/* Callbak interfaces to delegate installation of missing extensions */
	@SuppressWarnings("rawtypes")
	private static Vector providers;

	/**
	 * <p>
	 * Register an ExtensionInstallationProvider. The provider is responsible
	 * for handling the installation (upgrade) of any missing extensions.
	 * </p>
	 * 
	 * @param eip
	 *            ExtensionInstallationProvider implementation
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public synchronized static void addExtensionInstallationProvider(
			ExtensionInstallationProvider eip) {
		if (providers == null) {
			providers = new Vector();
		}
		providers.add(eip);
	}

	/**
	 * <p>
	 * Unregister a previously installed installation provider
	 * </p>
	 */
	public synchronized static void removeExtensionInstallationProvider(
			ExtensionInstallationProvider eip) {
		providers.remove(eip);
	}

	/**
	 * <p>
	 * Checks the dependencies of the jar file on installed extension.
	 * </p>
	 * 
	 * @param jarFile
	 *            containing the attriutes declaring the dependencies
	 */
	public static boolean checkExtensionsDependencies(JarFile jar) {
		if (providers == null) {
			// no need to bother, nobody is registered to install missing
			// extensions
			return true;
		}

		try {
			ExtensionDependency extDep = new ExtensionDependency();
			return extDep.checkExtensions(jar);
		} catch (ExtensionInstallationException e) {
			debug(e.getMessage());
		}
		return false;
	}

	/*
	 * Check for all declared required extensions in the jar file manifest.
	 */
	protected boolean checkExtensions(JarFile jar)
			throws ExtensionInstallationException {
		Manifest man;
		try {
			man = jar.getManifest();
		} catch (IOException e) {
			return false;
		}

		if (man == null) {
			// The applet does not define a manifest file, so
			// we just assume all dependencies are satisfied.
			return true;
		}

		boolean result = true;
		Attributes attr = man.getMainAttributes();
		if (attr != null) {
			// Let's get the list of declared dependencies
			String value = attr.getValue(Name.EXTENSION_LIST);
			if (value != null) {
				StringTokenizer st = new StringTokenizer(value);
				// Iterate over all declared dependencies
				while (st.hasMoreTokens()) {
					String extensionName = st.nextToken();
					debug("The file " + jar.getName()
							+ " appears to depend on " + extensionName);
					// Sanity Check
					String extName = extensionName + "-"
							+ Name.EXTENSION_NAME.toString();
					if (attr.getValue(extName) == null) {
						debug("The jar file " + jar.getName()
								+ " appers to depend on " + extensionName
								+ " but does not define the " + extName
								+ " attribute in its manifest ");

					} else {
						if (!checkExtension(extensionName, attr)) {
							debug("Failed installing " + extensionName);
							result = false;
						}
					}
				}
			} else {
				debug("No dependencies for " + jar.getName());
			}
		}
		return result;
	}

	/*
	 * <p> Check that a particular dependency on an extension is satisfied. </p>
	 * 
	 * @param extensionName is the key used for the attributes in the manifest
	 * 
	 * @param attr is the attributes of the manifest file
	 * 
	 * @return true if the dependency is satisfied by the installed extensions
	 */
	protected synchronized boolean checkExtension(final String extensionName,
			final Attributes attr) throws ExtensionInstallationException {
		debug("Checking extension " + extensionName);
		if (checkExtensionAgainstInstalled(extensionName, attr))
			return true;

		debug("Extension not currently installed ");
		ExtensionInfo reqInfo = new ExtensionInfo(extensionName, attr);
		return installExtension(reqInfo, null);
	}

	/*
	 * <p> Check if a particular extension is part of the currently installed
	 * extensions. </p>
	 * 
	 * @param extensionName is the key for the attributes in the manifest
	 * 
	 * @param attr is the attributes of the manifest
	 * 
	 * @return true if the requested extension is already installed
	 */
	boolean checkExtensionAgainstInstalled(String extensionName, Attributes attr)
			throws ExtensionInstallationException {
		File fExtension = checkExtensionExists(extensionName);

		if (fExtension != null) {
			// Extension already installed, just check against this one
			try {
				if (checkExtensionAgainst(extensionName, attr, fExtension))
					return true;
			} catch (FileNotFoundException e) {
				debugException(e);
			} catch (IOException e) {
				debugException(e);
			}
			return false;

		} else {
			// Not sure if extension is already installed, so check all the
			// installed extension jar files to see if we get a match

			File[] installedExts;

			try {
				// Get the list of installed extension jar files so we can
				// compare the installed versus the requested extension
				installedExts = getInstalledExtensions();
			} catch (IOException e) {
				debugException(e);
				return false;
			}

			for (int i = 0; i < installedExts.length; i++) {
				try {
					if (checkExtensionAgainst(extensionName, attr,
							installedExts[i]))
						return true;
				} catch (FileNotFoundException e) {
					debugException(e);
				} catch (IOException e) {
					debugException(e);
					// let's continue with the next installed extension
				}
			}
		}
		return false;
	}

	/*
	 * <p> Check if the requested extension described by the attributes in the
	 * manifest under the key extensionName is compatible with the jar file.
	 * </p>
	 * 
	 * @param extensionName key in the attibute list
	 * 
	 * @param attr manifest file attributes
	 * 
	 * @param file installed extension jar file to compare the requested
	 * extension against.
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	protected boolean checkExtensionAgainst(String extensionName,
			Attributes attr, final File file) throws IOException,
			FileNotFoundException, ExtensionInstallationException {

		debug("Checking extension " + extensionName + " against "
				+ file.getName());

		// Load the jar file ...
		Manifest man;
		try {
			man = (Manifest) AccessController
					.doPrivileged(new PrivilegedExceptionAction() {
						public Object run() throws IOException,
								FileNotFoundException {
							if (!file.exists())
								throw new FileNotFoundException(file.getName());
							JarFile jarFile = new JarFile(file);
							return jarFile.getManifest();
						}
					});
		} catch (PrivilegedActionException e) {
			if (e.getException() instanceof FileNotFoundException)
				throw (FileNotFoundException) e.getException();
			throw (IOException) e.getException();
		}

		// Construct the extension information object
		ExtensionInfo reqInfo = new ExtensionInfo(extensionName, attr);
		debug("Requested Extension : " + reqInfo);

		int isCompatible = ExtensionInfo.INCOMPATIBLE;
		ExtensionInfo instInfo = null;

		if (man != null) {
			Attributes instAttr = man.getMainAttributes();
			if (instAttr != null) {
				instInfo = new ExtensionInfo(null, instAttr);
				debug("Extension Installed " + instInfo);
				isCompatible = instInfo.isCompatibleWith(reqInfo);
				switch (isCompatible) {
				case ExtensionInfo.COMPATIBLE:
					debug("Extensions are compatible");
					return true;

				case ExtensionInfo.INCOMPATIBLE:
					debug("Extensions are incompatible");
					return false;

				default:
					// everything else
					debug("Extensions require an upgrade or vendor switch");
					return installExtension(reqInfo, instInfo);

				}
			}
		}
		return false;
	}

	/*
	 * <p> An required extension is missing, if an ExtensionInstallationProvider
	 * is registered, delegate the installation of that particular extension to
	 * it. </p>
	 * 
	 * @param reqInfo Missing extension information
	 * 
	 * @param instInfo Older installed version information
	 * 
	 * @return true if the installation is successful
	 */
	@SuppressWarnings("rawtypes")
	protected boolean installExtension(ExtensionInfo reqInfo,
			ExtensionInfo instInfo) throws ExtensionInstallationException {

		Vector currentProviders;
		synchronized (providers) {
			currentProviders = (Vector) providers.clone();
		}
		for (Enumeration e = currentProviders.elements(); e.hasMoreElements();) {
			ExtensionInstallationProvider eip = (ExtensionInstallationProvider) e
					.nextElement();

			if (eip != null) {
				// delegate the installation to the provider
				if (eip.installExtension(reqInfo, instInfo)) {
					debug(reqInfo.name + " installation successful");
					Launcher.ExtClassLoader cl = (Launcher.ExtClassLoader) Launcher
							.getLauncher().getClassLoader().getParent();
					addNewExtensionsToClassLoader(cl);
					return true;
				}
			}
		}
		// We have tried all of our providers, noone could install this
		// extension, we just return failure at this point
		debug(reqInfo.name + " installation failed");
		return false;
	}

	/**
	 * <p>
	 * Checks if the extension, that is specified in the extension-list in the
	 * applet jar manifest, is already installed (i.e. exists in the extension
	 * directory).
	 * </p>
	 * 
	 * @param extensionName
	 *            extension name in the extension-list
	 * 
	 * @return the extension if it exists in the extension directory
	 */
	@SuppressLint("DefaultLocale")
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private File checkExtensionExists(String extensionName) {
		// Function added to fix bug 4504166
		final String extName = extensionName;
		final String[] fileExt = { ".jar", ".zip" };

		return (File) AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
				try {
					File fExtension;
					File[] dirs = getExtDirs();

					// Search the extension directories for the extension that
					// is specified
					// in the attribute extension-list in the applet jar
					// manifest
					for (int i = 0; i < dirs.length; i++) {
						for (int j = 0; j < fileExt.length; j++) {
							if (extName.toLowerCase().endsWith(fileExt[j])) {
								fExtension = new File(dirs[i], extName);
							} else {
								fExtension = new File(dirs[i], extName
										+ fileExt[j]);
							}
							debug("checkExtensionExists:fileName "
									+ fExtension.getName());
							if (fExtension.exists()) {
								return fExtension;
							}
						}
					}
					return null;

				} catch (Exception e) {
					debugException(e);
					return null;
				}
			}
		});
	}

	/**
	 * <p>
	 * 
	 * @return the java.ext.dirs property as a list of directory
	 *         </p>
	 */
	@SuppressWarnings("unused")
	private static File[] getExtDirs() {
		// HACK
		String s = "";// java.security.AccessController.doPrivileged(new
						// sun.security.action.GetPropertyAction("java.ext.dirs"));

		File[] dirs;
		if (s != null) {
			StringTokenizer st = new StringTokenizer(s, File.pathSeparator);
			int count = st.countTokens();
			debug("getExtDirs count " + count);
			dirs = new File[count];
			for (int i = 0; i < count; i++) {
				dirs[i] = new File(st.nextToken());
				debug("getExtDirs dirs[" + i + "] " + dirs[i]);
			}
		} else {
			dirs = new File[0];
			debug("getExtDirs dirs " + dirs);
		}
		debug("getExtDirs dirs.length " + dirs.length);
		return dirs;
	}

	/*
	 * <p> Scan the directories and return all files installed in those </p>
	 * 
	 * @param dirs list of directories to scan
	 * 
	 * @return the list of files installed in all the directories
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private static File[] getExtFiles(File[] dirs) throws IOException {
		Vector urls = new Vector();
		for (int i = 0; i < dirs.length; i++) {
			String[] files = dirs[i].list(new JarFilter());
			if (files != null) {
				debug("getExtFiles files.length " + files.length);
				for (int j = 0; j < files.length; j++) {
					File f = new File(dirs[i], files[j]);
					urls.add(f);
					debug("getExtFiles f[" + j + "] " + f);
				}
			}
		}
		File[] ua = new File[urls.size()];
		urls.copyInto(ua);
		debug("getExtFiles ua.length " + ua.length);
		return ua;
	}

	/*
	 * <p>
	 * 
	 * @return the list of installed extensions jar files </p>
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private File[] getInstalledExtensions() throws IOException {
		return (File[]) AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
				try {
					return getExtFiles(getExtDirs());
				} catch (IOException e) {
					debug("Cannot get list of installed extensions");
					debugException(e);
					return new URL[0];
				}
			}
		});
	}

	/*
	 * <p> Add the newly installed jar file to the extension class loader. </p>
	 * 
	 * @param cl the current installed extension class loader
	 * 
	 * @return true if successful
	 */
	private Boolean addNewExtensionsToClassLoader(Launcher.ExtClassLoader cl) {
		try {
			File[] installedExts = getInstalledExtensions();
			for (int i = 0; i < installedExts.length; i++) {
				final File instFile = installedExts[i];
				URL instURL = AccessController
						.doPrivileged(new PrivilegedAction<URL>() {
							public URL run() {
								try {
									return ParseUtil.fileToEncodedURL(instFile);
								} catch (MalformedURLException e) {
									debugException(e);
									return null;
								}
							}
						});
				if (instURL != null) {
					URL[] urls = cl.getURLs();
					boolean found = false;
					for (int j = 0; j < urls.length; j++) {
						debug("URL[" + j + "] is " + urls[j] + " looking for "
								+ instURL);
						if (urls[j].toString().compareToIgnoreCase(
								instURL.toString()) == 0) {
							found = true;
							debug("Found !");
						}
					}
					if (!found) {
						debug("Not Found ! adding to the classloader "
								+ instURL);
						cl.addExtURL(instURL);
					}
				}
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
			// let's continue with the next installed extension
		}
		return Boolean.TRUE;
	}

	// True to display all debug and trace messages
	static final boolean DEBUG = false;

	private static void debug(String s) {
		if (DEBUG) {
			System.err.println(s);
		}
	}

	private void debugException(Throwable e) {
		if (DEBUG) {
			e.printStackTrace();
		}
	}

}
